"""
Enhanced Dependency Validation System

Provides comprehensive dependency checking with detailed error messages,
automatic fixing suggestions, and support for complex requirements.
"""

import os
import shutil
import subprocess
import re
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple, Set, Union
from dataclasses import dataclass, field
from enum import Enum
import json

logger = logging.getLogger(__name__)


class DependencyType(Enum):
    """Types of dependencies that can be validated."""
    EXECUTABLE = "executable"
    LIBRARY = "library"
    ENVIRONMENT_VARIABLE = "environment_variable"
    FILE = "file"
    DIRECTORY = "directory"
    PYTHON_PACKAGE = "python_package"
    SYSTEM_PACKAGE = "system_package"
    VERSION_CHECK = "version_check"
    CUSTOM_CHECK = "custom_check"


class ValidationSeverity(Enum):
    """Severity levels for validation issues."""
    CRITICAL = "critical"    # Must be fixed for extension to work
    WARNING = "warning"      # Should be fixed for optimal performance
    INFO = "info"           # Nice to have, but not required


@dataclass
class DependencyRequirement:
    """A single dependency requirement."""
    name: str
    type: DependencyType
    required: bool = True
    severity: ValidationSeverity = ValidationSeverity.CRITICAL
    
    # Version constraints
    minimum_version: Optional[str] = None
    maximum_version: Optional[str] = None
    exact_version: Optional[str] = None
    
    # Check configuration
    check_command: Optional[List[str]] = None
    check_path: Optional[str] = None
    environment_variable: Optional[str] = None
    
    # Validation patterns
    version_pattern: Optional[str] = None
    output_pattern: Optional[str] = None
    
    # Fix suggestions
    install_command: Optional[List[str]] = None
    install_url: Optional[str] = None
    install_notes: Optional[str] = None
    
    # Custom validation
    custom_validator: Optional[str] = None  # Function name for custom validation
    
    # Metadata
    description: Optional[str] = None
    documentation_url: Optional[str] = None


@dataclass
class ValidationResult:
    """Result of dependency validation."""
    requirement: DependencyRequirement
    satisfied: bool
    found_version: Optional[str] = None
    actual_value: Optional[Any] = None
    error_message: Optional[str] = None
    fix_suggestions: List[str] = field(default_factory=list)
    additional_info: Dict[str, Any] = field(default_factory=dict)


@dataclass
class ValidationSummary:
    """Summary of all dependency validations."""
    total_requirements: int
    satisfied_requirements: int
    critical_issues: int
    warnings: int
    info_issues: int
    results: List[ValidationResult] = field(default_factory=list)
    overall_status: str = "unknown"
    
    def __post_init__(self):
        """Calculate overall status."""
        if self.critical_issues > 0:
            self.overall_status = "failed"
        elif self.warnings > 0:
            self.overall_status = "warnings"
        else:
            self.overall_status = "passed"


class EnhancedDependencyValidator:
    """
    Enhanced dependency validation system with comprehensive checking
    and intelligent fix suggestions.
    """
    
    def __init__(self):
        """Initialize the dependency validator."""
        self.custom_validators: Dict[str, callable] = {}
        self.system_info = self._gather_system_info()
        
    def register_custom_validator(self, name: str, validator_func: callable) -> None:
        """Register a custom validation function."""
        self.custom_validators[name] = validator_func
        logger.debug(f"Registered custom validator: {name}")
        
    def validate_requirements(self, requirements: List[DependencyRequirement]) -> ValidationSummary:
        """
        Validate a list of dependency requirements.
        
        Args:
            requirements: List of requirements to validate
            
        Returns:
            Validation summary with detailed results
        """
        results = []
        critical_issues = 0
        warnings = 0
        info_issues = 0
        
        logger.info(f"Validating {len(requirements)} dependency requirements")
        
        for requirement in requirements:
            result = self._validate_single_requirement(requirement)
            results.append(result)
            
            if not result.satisfied:
                if result.requirement.severity == ValidationSeverity.CRITICAL:
                    critical_issues += 1
                elif result.requirement.severity == ValidationSeverity.WARNING:
                    warnings += 1
                else:
                    info_issues += 1
                    
        satisfied_count = sum(1 for r in results if r.satisfied)
        
        summary = ValidationSummary(
            total_requirements=len(requirements),
            satisfied_requirements=satisfied_count,
            critical_issues=critical_issues,
            warnings=warnings,
            info_issues=info_issues,
            results=results
        )
        
        logger.info(f"Validation complete: {satisfied_count}/{len(requirements)} satisfied, "
                   f"{critical_issues} critical issues")
        
        return summary
        
    def _validate_single_requirement(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate a single dependency requirement."""
        logger.debug(f"Validating requirement: {requirement.name} ({requirement.type.value})")
        
        try:
            if requirement.type == DependencyType.EXECUTABLE:
                return self._validate_executable(requirement)
            elif requirement.type == DependencyType.LIBRARY:
                return self._validate_library(requirement)
            elif requirement.type == DependencyType.ENVIRONMENT_VARIABLE:
                return self._validate_environment_variable(requirement)
            elif requirement.type == DependencyType.FILE:
                return self._validate_file(requirement)
            elif requirement.type == DependencyType.DIRECTORY:
                return self._validate_directory(requirement)
            elif requirement.type == DependencyType.PYTHON_PACKAGE:
                return self._validate_python_package(requirement)
            elif requirement.type == DependencyType.SYSTEM_PACKAGE:
                return self._validate_system_package(requirement)
            elif requirement.type == DependencyType.VERSION_CHECK:
                return self._validate_version_check(requirement)
            elif requirement.type == DependencyType.CUSTOM_CHECK:
                return self._validate_custom_check(requirement)
            else:
                return ValidationResult(
                    requirement=requirement,
                    satisfied=False,
                    error_message=f"Unknown dependency type: {requirement.type}"
                )
                
        except Exception as e:
            logger.error(f"Error validating requirement {requirement.name}: {e}")
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Validation error: {str(e)}"
            )
            
    def _validate_executable(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate that an executable is available."""
        executable_path = shutil.which(requirement.name)
        
        if not executable_path:
            suggestions = []
            if requirement.install_command:
                suggestions.append(f"Install using: {' '.join(requirement.install_command)}")
            if requirement.install_url:
                suggestions.append(f"Download from: {requirement.install_url}")
            if not suggestions:
                suggestions.append(f"Install {requirement.name} and ensure it's in your PATH")
                
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Executable '{requirement.name}' not found in PATH",
                fix_suggestions=suggestions
            )
            
        # Check version if required
        version_info = {}
        if requirement.minimum_version or requirement.exact_version or requirement.maximum_version:
            version_result = self._check_executable_version(requirement, executable_path)
            if not version_result.satisfied:
                return version_result
            version_info = version_result.additional_info
            
        return ValidationResult(
            requirement=requirement,
            satisfied=True,
            actual_value=executable_path,
            additional_info=version_info
        )
        
    def _validate_library(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate that a library is available."""
        # Try pkg-config first
        if requirement.check_command:
            try:
                result = subprocess.run(
                    requirement.check_command,
                    capture_output=True,
                    text=True,
                    timeout=30
                )
                if result.returncode == 0:
                    return ValidationResult(
                        requirement=requirement,
                        satisfied=True,
                        actual_value=result.stdout.strip()
                    )
            except (subprocess.TimeoutExpired, subprocess.SubprocessError):
                pass
                
        # Try to find library files
        if requirement.check_path:
            lib_path = Path(requirement.check_path)
            if lib_path.exists():
                return ValidationResult(
                    requirement=requirement,
                    satisfied=True,
                    actual_value=str(lib_path)
                )
                
        suggestions = []
        if requirement.install_command:
            suggestions.append(f"Install using: {' '.join(requirement.install_command)}")
        else:
            suggestions.append(f"Install development package for {requirement.name}")
            
        return ValidationResult(
            requirement=requirement,
            satisfied=False,
            error_message=f"Library '{requirement.name}' not found",
            fix_suggestions=suggestions
        )
        
    def _validate_environment_variable(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate that an environment variable is set."""
        env_var = requirement.environment_variable or requirement.name
        value = os.environ.get(env_var)
        
        if not value:
            suggestions = [f"Set environment variable: export {env_var}=<path>"]
            if requirement.install_notes:
                suggestions.append(requirement.install_notes)
                
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Environment variable '{env_var}' is not set",
                fix_suggestions=suggestions
            )
            
        # Validate the path if it should point to a file/directory
        if requirement.check_path:
            check_path = Path(value) / requirement.check_path
            if not check_path.exists():
                return ValidationResult(
                    requirement=requirement,
                    satisfied=False,
                    error_message=f"Path specified by {env_var} is invalid: {check_path} not found",
                    fix_suggestions=[f"Ensure {env_var} points to the correct installation directory"]
                )
                
        return ValidationResult(
            requirement=requirement,
            satisfied=True,
            actual_value=value
        )
        
    def _validate_file(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate that a file exists."""
        file_path = Path(requirement.check_path or requirement.name)
        
        if not file_path.exists():
            suggestions = [f"Ensure file exists at: {file_path}"]
            if requirement.install_notes:
                suggestions.append(requirement.install_notes)
                
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Required file not found: {file_path}",
                fix_suggestions=suggestions
            )
            
        return ValidationResult(
            requirement=requirement,
            satisfied=True,
            actual_value=str(file_path.resolve())
        )
        
    def _validate_directory(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate that a directory exists."""
        dir_path = Path(requirement.check_path or requirement.name)
        
        if not dir_path.exists():
            suggestions = [f"Create directory: mkdir -p {dir_path}"]
            if requirement.install_notes:
                suggestions.append(requirement.install_notes)
                
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Required directory not found: {dir_path}",
                fix_suggestions=suggestions
            )
            
        if not dir_path.is_dir():
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Path exists but is not a directory: {dir_path}",
                fix_suggestions=[f"Remove file and create directory: rm {dir_path} && mkdir -p {dir_path}"]
            )
            
        return ValidationResult(
            requirement=requirement,
            satisfied=True,
            actual_value=str(dir_path.resolve())
        )
        
    def _validate_python_package(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate that a Python package is available."""
        try:
            import importlib
            module = importlib.import_module(requirement.name)
            
            # Try to get version
            version = None
            for attr in ['__version__', 'version', 'VERSION']:
                if hasattr(module, attr):
                    version = str(getattr(module, attr))
                    break
                    
            return ValidationResult(
                requirement=requirement,
                satisfied=True,
                found_version=version,
                actual_value=str(module.__file__) if hasattr(module, '__file__') else "built-in"
            )
            
        except ImportError:
            suggestions = [f"Install using: pip install {requirement.name}"]
            if requirement.install_command:
                suggestions = [f"Install using: {' '.join(requirement.install_command)}"]
                
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Python package '{requirement.name}' not found",
                fix_suggestions=suggestions
            )
            
    def _validate_system_package(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate that a system package is installed."""
        # This is a simplified implementation - in practice, you'd check different package managers
        if requirement.check_command:
            try:
                result = subprocess.run(
                    requirement.check_command,
                    capture_output=True,
                    text=True,
                    timeout=30
                )
                if result.returncode == 0:
                    return ValidationResult(
                        requirement=requirement,
                        satisfied=True,
                        actual_value=result.stdout.strip()
                    )
            except (subprocess.TimeoutExpired, subprocess.SubprocessError):
                pass
                
        suggestions = []
        if requirement.install_command:
            suggestions.append(f"Install using: {' '.join(requirement.install_command)}")
        else:
            # Provide system-specific suggestions
            if 'ubuntu' in self.system_info.get('os', '').lower():
                suggestions.append(f"Install using: sudo apt-get install {requirement.name}")
            elif 'centos' in self.system_info.get('os', '').lower():
                suggestions.append(f"Install using: sudo yum install {requirement.name}")
            elif 'fedora' in self.system_info.get('os', '').lower():
                suggestions.append(f"Install using: sudo dnf install {requirement.name}")
            else:
                suggestions.append(f"Install system package: {requirement.name}")
                
        return ValidationResult(
            requirement=requirement,
            satisfied=False,
            error_message=f"System package '{requirement.name}' not found",
            fix_suggestions=suggestions
        )
        
    def _validate_version_check(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate using a custom version check command."""
        if not requirement.check_command:
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message="Version check requires check_command to be specified"
            )
            
        try:
            result = subprocess.run(
                requirement.check_command,
                capture_output=True,
                text=True,
                timeout=30
            )
            
            if result.returncode != 0:
                return ValidationResult(
                    requirement=requirement,
                    satisfied=False,
                    error_message=f"Version check command failed: {result.stderr}",
                    fix_suggestions=[f"Ensure {requirement.name} is properly installed"]
                )
                
            # Extract version using pattern
            version = None
            if requirement.version_pattern:
                match = re.search(requirement.version_pattern, result.stdout)
                if match:
                    version = match.group(1) if match.groups() else match.group(0)
                    
            # Check version constraints
            if version and (requirement.minimum_version or requirement.exact_version or requirement.maximum_version):
                version_valid = self._check_version_constraints(version, requirement)
                if not version_valid:
                    return ValidationResult(
                        requirement=requirement,
                        satisfied=False,
                        found_version=version,
                        error_message=f"Version {version} does not meet requirements",
                        fix_suggestions=[f"Install compatible version of {requirement.name}"]
                    )
                    
            return ValidationResult(
                requirement=requirement,
                satisfied=True,
                found_version=version,
                actual_value=result.stdout.strip()
            )
            
        except subprocess.TimeoutExpired:
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message="Version check command timed out"
            )
        except Exception as e:
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Version check failed: {str(e)}"
            )
            
    def _validate_custom_check(self, requirement: DependencyRequirement) -> ValidationResult:
        """Validate using a custom validation function."""
        if not requirement.custom_validator:
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message="Custom check requires custom_validator to be specified"
            )
            
        validator_func = self.custom_validators.get(requirement.custom_validator)
        if not validator_func:
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Custom validator '{requirement.custom_validator}' not found"
            )
            
        try:
            return validator_func(requirement)
        except Exception as e:
            return ValidationResult(
                requirement=requirement,
                satisfied=False,
                error_message=f"Custom validation failed: {str(e)}"
            )
            
    def _check_executable_version(self, requirement: DependencyRequirement, executable_path: str) -> ValidationResult:
        """Check the version of an executable."""
        if not requirement.check_command:
            # Try common version commands
            version_commands = [
                [executable_path, '--version'],
                [executable_path, '-version'],
                [executable_path, 'version'],
                [executable_path, '-V']
            ]
        else:
            version_commands = [requirement.check_command]
            
        for cmd in version_commands:
            try:
                result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
                if result.returncode == 0:
                    # Extract version using pattern
                    version = None
                    output = result.stdout + result.stderr
                    
                    if requirement.version_pattern:
                        match = re.search(requirement.version_pattern, output)
                        if match:
                            version = match.group(1) if match.groups() else match.group(0)
                    else:
                        # Try common version patterns
                        patterns = [
                            r'(\d+\.\d+\.\d+)',
                            r'version\s+(\d+\.\d+\.\d+)',
                            r'v(\d+\.\d+\.\d+)',
                            r'(\d+\.\d+)'
                        ]
                        for pattern in patterns:
                            match = re.search(pattern, output, re.IGNORECASE)
                            if match:
                                version = match.group(1)
                                break
                                
                    if version:
                        # Check version constraints
                        if self._check_version_constraints(version, requirement):
                            return ValidationResult(
                                requirement=requirement,
                                satisfied=True,
                                found_version=version,
                                additional_info={'version_output': output.strip()}
                            )
                        else:
                            return ValidationResult(
                                requirement=requirement,
                                satisfied=False,
                                found_version=version,
                                error_message=f"Version {version} does not meet requirements",
                                fix_suggestions=[f"Install compatible version of {requirement.name}"]
                            )
                    break
            except (subprocess.TimeoutExpired, subprocess.SubprocessError):
                continue
                
        return ValidationResult(
            requirement=requirement,
            satisfied=False,
            error_message=f"Could not determine version of {requirement.name}"
        )
        
    def _check_version_constraints(self, version: str, requirement: DependencyRequirement) -> bool:
        """Check if a version meets the specified constraints."""
        try:
            from packaging import version as pkg_version
            
            v = pkg_version.parse(version)
            
            if requirement.exact_version:
                return v == pkg_version.parse(requirement.exact_version)
                
            if requirement.minimum_version:
                if v < pkg_version.parse(requirement.minimum_version):
                    return False
                    
            if requirement.maximum_version:
                if v > pkg_version.parse(requirement.maximum_version):
                    return False
                    
            return True
            
        except ImportError:
            # Fallback to simple string comparison if packaging is not available
            logger.warning("packaging module not available, using simple version comparison")
            
            if requirement.exact_version:
                return version == requirement.exact_version
            # Simple lexicographic comparison (not always accurate)
            if requirement.minimum_version:
                if version < requirement.minimum_version:
                    return False
            if requirement.maximum_version:
                if version > requirement.maximum_version:
                    return False
                    
            return True
            
        except Exception as e:
            logger.warning(f"Error comparing versions: {e}")
            return True  # Assume satisfied if we can't compare
            
    def _gather_system_info(self) -> Dict[str, str]:
        """Gather basic system information for platform-specific suggestions."""
        import platform
        
        info = {
            'system': platform.system().lower(),
            'machine': platform.machine().lower(),
            'python_version': platform.python_version()
        }
        
        # Try to determine Linux distribution
        if info['system'] == 'linux':
            try:
                with open('/etc/os-release', 'r') as f:
                    for line in f:
                        if line.startswith('ID='):
                            info['os'] = line.split('=')[1].strip().strip('"')
                            break
            except FileNotFoundError:
                pass
                
        return info
        
    def generate_install_script(self, summary: ValidationSummary, output_path: Optional[Path] = None) -> Optional[str]:
        """Generate a shell script to install missing dependencies."""
        if summary.critical_issues == 0 and summary.warnings == 0:
            return None
            
        script_lines = [
            "#!/bin/bash",
            "# Auto-generated dependency installation script",
            "# Generated by GSM Enhanced Dependency Validator",
            "",
            "set -e  # Exit on any error",
            "",
            "echo 'Installing missing dependencies...'",
            ""
        ]
        
        for result in summary.results:
            if not result.satisfied and result.fix_suggestions:
                script_lines.append(f"# {result.requirement.name}")
                if result.requirement.description:
                    script_lines.append(f"# {result.requirement.description}")
                    
                # Add install commands from suggestions
                for suggestion in result.fix_suggestions:
                    if suggestion.startswith(('Install using:', 'sudo ', 'pip install', 'apt-get', 'yum ', 'dnf ')):
                        command = suggestion.replace('Install using: ', '')
                        script_lines.append(f"echo 'Installing {result.requirement.name}...'")
                        script_lines.append(f"{command}")
                        script_lines.append("")
                        
        script_lines.extend([
            "echo 'Dependency installation complete!'",
            "echo 'Please run the validation again to verify all dependencies are satisfied.'"
        ])
        
        script_content = '\n'.join(script_lines)
        
        if output_path:
            output_path.write_text(script_content)
            output_path.chmod(0o755)  # Make executable
            logger.info(f"Install script written to: {output_path}")
            
        return script_content